fix: provenance-file takes precedence over OIDC auto-generated provenance - #9947
Merged
Conversation
…ance (#9882) ## Why Needed When publishing with an externally generated provenance bundle under OIDC trusted publishing (`npm publish <tarball> --provenance-file=<bundle>`), npm silently discarded the supplied bundle and published its own auto-generated provenance instead. Three layers interacted: 1. `lib/utils/oidc.js` auto-enabled provenance (`opts.provenance = true`) whenever the `provenance` config was at its default, without checking whether a `provenance-file` was supplied. 2. In `libnpmpublish`'s `buildMetadata()`, the inner `provenance === true` branch then ran `generateProvenance()`, so the `verifyProvenance(subject, provenanceFile)` branch, the only code path that reads the supplied file, never executed. 3. Every documented way to disable automatic provenance was unusable in combination with `--provenance-file` (config-layer mutual exclusivity error, env carve-out, or publishConfig flatten timing). ## What Changes - **`lib/utils/oidc.js`**: skip auto-enabling provenance when a provenance file is configured (`opts.provenanceFile`). `opts` already carries `provenanceFile` from every config source (CLI/env/npmrc/publishConfig) by the time the OIDC flow runs, so this covers all entry paths. The supplied bundle is now verified via `verifyProvenance()` and published, as documented. Automatic provenance is also no longer written to the shared config; it is set only on the current publish's `opts` (which reaches libnpmpublish via `otplease`). Previously the `user`-scoped config write leaked `provenance: true` into later workspace publishes during `npm publish --workspaces`, where a workspace with `publishConfig["provenance-file"]` would then hit the conflict check below. - **`libnpmpublish`**: `buildMetadata()` now throws ~~`EPROVENANCECONFLICT`~~ `EUSAGE` (updated per review) when both `provenance: true` and `provenanceFile` are provided. - **Docs**: config descriptions for `provenance` / `provenance-file` and the libnpmpublish README now state the precedence rule. ###⚠️ ~~New error code (feedback requested)~~ Resolved: reuses `EUSAGE` ~~This PR introduces `EPROVENANCECONFLICT` in libnpmpublish, thrown when both `provenance: true` and `provenanceFile` are provided programmatically.~~ ~~**Rationale:** the README already documents the two as mutually exclusive, and silently preferring either direction discards a cryptographically meaningful artifact. Note the CLI's config layer reports the same conflict as a `TypeError` without an error code (pre-existing). Happy to align on `EUSAGE` or another convention per review.~~ **Update:** per review, the conflict reuses the existing `EUSAGE` code instead of introducing a new one (`Object.assign(new Error('provenance and provenanceFile cannot be used together'), { code: 'EUSAGE' })`), and the README documents `EUSAGE` accordingly. ## Testing - New CLI regression test: OIDC trusted publishing + `provenance-file` ~~config~~ asserts the published packument's sigstore attachment deep-equals the supplied bundle (and that sigstore generation is never invoked). **Update:** parameterized per review to cover both `provenance-file` sources, CLI config and `publishConfig`; the latter proves `publishConfig["provenance-file"]` is flattened into `opts.provenanceFile` via `Publish.#getManifest()` before `oidc()` decides whether to enable automatic provenance. - New libnpmpublish test: both options set → rejects with ~~`EPROVENANCECONFLICT`~~ `EUSAGE`, no registry PUT, generation not invoked. - New workspace regression test (per review): publishes two public workspaces in order (one where OIDC auto-enables provenance, then one with `publishConfig["provenance-file"]`), and asserts each publish receives exactly its own options (`{ provenance: true, provenanceFile: null }` then `{ provenance: false, provenanceFile }`) and that the shared config stays at its default (`npm.config.isDefault('provenance') === true`). Verified to fail on the pre-fix code for exactly the leak reason, and to pass after the fix. `mock-oidc` gained a `times` option on the GitHub id-token mock to serve one token request per workspace. - Full root suite green with 100% coverage; libnpmpublish workspace suite green; lint clean. ## References Fixes #9879 ## Out of scope (noted for follow-up) - `publishConfig.provenance: false` does not block the OIDC auto-enable (publishConfig flattens into `opts` only, so `config.isDefault('provenance')` stays true). A separate behavioral question about `isDefault` semantics. - Hardening `config.set` itself against bypassing load-time exclusivity is an `@npmcli/config` semver-major conversation; this flow no longer writes `provenance` to the config at all. --------- Signed-off-by: Yunseo Kim <[email protected]> (cherry picked from commit c9876d7)
martinrrm
approved these changes
Sep 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Backport of #9882 to
release/v11.